﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using STSdb4.Database;
using fastJSON;
using System.IO;

namespace Com.SuperCache.Engine
{
    public class RawCache : BaseCache
    {
        private const string ExpirationFileExtension = "exp";
        private const string DataFileExtension = "dat";
        private const string statFile = "SuperCache.sta";
        private string dataPath;
        private static Dictionary<string, object> memoryData = new Dictionary<string, object>();
        private static Dictionary<string, DateTime?> memoryExpiration = new Dictionary<string, DateTime?>();
        private static object syncRoot = new object();
        private bool isMemory = false;
        private RecycleAlgorithms recycleAlgorithm;
        private int maxCount;
        private int threshold;
        private static Dictionary<string, KeyValue> usageStat = new Dictionary<string, KeyValue>();
        private Dictionary<string, KeyValuePair<DateTime, string>> expiredFiles = new Dictionary<string, KeyValuePair<DateTime, string>>();
        private RecycleModes recycleMode;

        public RawCache(string DataPath, RecycleAlgorithms RecycleAlgorithm, int MaxCount, int Threshold, RecycleModes RecycleMode)
        {
            dataPath = DataPath;
            if (!dataPath.EndsWith(Path.DirectorySeparatorChar.ToString()))
                dataPath += Path.DirectorySeparatorChar;

            isMemory = string.IsNullOrEmpty(DataPath);

            recycleAlgorithm = RecycleAlgorithm;
            maxCount = MaxCount;
            threshold = Threshold;
            recycleMode = RecycleMode;
        }

        public override void Add<K>(string Category, K Key, object Data)
        {
            Add(Category, Key, Data, null);
        }

        private string GetExpirationTable(string Category)
        {
            return KeyExpiration + "_" + Category;
        }

        public override void Add<K, V>(string Category, IEnumerable<KeyValuePair<K, V>> Items, DateTime? ExpirationDate)
        {
            long count = 0;

            lock (syncRoot)
            {
                Items.ForEach(i =>
                {
                    var key = i.Key;
                    var data = i.Value;
                    var cacheKey = GetKey(Category, key.ToString());

                    if (isMemory)
                    {
                        memoryData[cacheKey] = data;
                        memoryExpiration[cacheKey] = ExpirationDate;

                        //recycle algo
                        switch (recycleAlgorithm)
                        {
                            case RecycleAlgorithms.MRU:
                                usageStat[cacheKey] = new KeyValue(string.Empty, 0);
                                if (recycleMode == RecycleModes.Active)
                                    Recycle<K>(Category, memoryData.Count);
                                break;
                            default:
                                break;
                        }
                    }
                    else
                    {
                        //will only serialize object other than string
                        var result = typeof(V) == typeof(string) ? data as string : JSON.Instance.ToJSON(data);
                        var fileKey = key.ToString();
                        var dataFile = GetFile(Category, fileKey, true);
                        bool exists = File.Exists(dataFile);
                        File.WriteAllText(dataFile, result);

                        //specify expiration
                        //default 30 mins to expire from now
                        var expirationDate = ExpirationDate == null || ExpirationDate <= DateTime.Now ? DateTime.Now.AddMinutes(30) : (DateTime)ExpirationDate;
                        var expirationFile = GetFile(Category, fileKey, false);
                        File.WriteAllText(expirationFile, expirationDate.ToString());

                        //recycle algo
                        if (recycleAlgorithm != RecycleAlgorithms.None)
                        {
                            var statFilePath = dataPath + statFile;
                            if (File.Exists(statFilePath))
                            {
                                var buffer = File.ReadAllText(statFilePath);
                                count = Convert.ToInt32(buffer);
                            }
                            if (!exists)
                            {
                                count++;
                                File.WriteAllText(statFilePath, count.ToString());
                            }
                            switch (recycleAlgorithm)
                            {
                                case RecycleAlgorithms.MRU:
                                    usageStat[cacheKey] = new KeyValue(expirationFile, 0);
                                    expiredFiles[cacheKey] = new KeyValuePair<DateTime, string>(expirationDate, expirationFile);
                                    if (recycleMode == RecycleModes.Active)
                                        Recycle<K>(Category, count);
                                    break;
                                default:
                                    break;
                            }
                        }
                    }
                });

                if (recycleAlgorithm != RecycleAlgorithms.None && recycleMode == RecycleModes.Passive)
                {
                    if (isMemory)
                        count = memoryData.Count;
                    Recycle<K>(Category, count);
                }
            }
        }

        public override void Add<K>(string Category, K Key, object Data, DateTime? ExpirationDate)
        {
            Add<K, object>(Category, new List<KeyValuePair<K, object>> { new KeyValuePair<K, object>(Key, Data) }, ExpirationDate);
        }

        private string GetFile(string Category, string FileName, bool IsData)
        {
            var path = dataPath + Category.NormalizeFileName() + @"\";
            if (!Directory.Exists(path))
                Directory.CreateDirectory(path);
            return path + FileName.NormalizeFileName() + "." + (IsData ? "dat" : ExpirationFileExtension);
        }

        private string GetKey(string Category, string Key)
        {
            return Category + "_" + Key;
        }

        public override List<KeyValuePair<K, V>> Get<K, V>(string Category, IEnumerable<K> Keys)
        {
            var result = new List<KeyValuePair<K, V>>();
            lock (syncRoot)
            {
                Keys.ForEach(key =>
                {
                    string buffer;
                    V value;
                    var cacheKey = GetKey(Category, key.ToString());
                    if (isMemory)
                    {
                        object memBuffer;
                        if (memoryData.TryGetValue(cacheKey, out memBuffer))
                        {
                            //track recycle
                            switch (recycleAlgorithm)
                            {
                                case RecycleAlgorithms.MRU:
                                    usageStat[cacheKey].Value++;
                                    break;
                                default:
                                    break;
                            }

                            value = (V)memBuffer;
                            DateTime? expirationDate;
                            if (memoryExpiration.TryGetValue(cacheKey, out expirationDate))
                            {
                                //expired
                                if (expirationDate != null && (DateTime)expirationDate < DateTime.Now)
                                {
                                    value = default(V);
                                    memoryData.Remove(cacheKey);
                                    memoryExpiration.Remove(cacheKey);
                                }
                            }
                        }
                        else
                            value = default(V);
                    }
                    else
                    {
                        var dataFilePath = GetFile(Category, key.ToString(), true);
                        if (File.Exists(dataFilePath))
                        {
                            buffer = File.ReadAllText(dataFilePath);

                            //track recycle
                            switch (recycleAlgorithm)
                            {
                                case RecycleAlgorithms.MRU:
                                    usageStat[cacheKey].Value++;
                                    break;
                                default:
                                    break;
                            }

                            //will only deserialize object other than string
                            value = typeof(V) == typeof(string) ? (V)(object)buffer : JSON.Instance.ToObject<V>(buffer);
                            DateTime expirationDate;
                            var expirationFilePath = GetFile(Category, key.ToString(), false);
                            if (File.Exists(expirationFilePath))
                            {
                                buffer = File.ReadAllText(expirationFilePath);
                                expirationDate = Convert.ToDateTime(buffer);
                                //expired
                                if (expirationDate < DateTime.Now)
                                {
                                    value = default(V);
                                    File.Delete(dataFilePath);
                                    File.Delete(expirationFilePath);
                                }
                            }
                        }
                        else
                            value = default(V);
                    }

                    result.Add(new KeyValuePair<K, V>(key, value));
                });
            }
            return result;
        }

        public override V Get<K, V>(string Category, K Key)
        {
            var buffer = Get<K, V>(Category, new K[] { Key });
            var result = buffer.FirstOrDefault();
            return result.Value;
        }

        public override void Recycle<K>(string Category, long Count)
        {
            if (Count < maxCount)
                return;

            switch (recycleAlgorithm)
            {
                case RecycleAlgorithms.MRU:
                    lock (syncRoot)
                    {
                        var recycledFileCount = 0;

                        if (isMemory)
                        {
                            //find out expired items
                            var memExpired = memoryExpiration.Where(e => e.Value != null && (DateTime)e.Value < DateTime.Now);
                            memExpired.ForEach(u =>
                            {
                                memoryData.Remove(u.Key);
                                memoryExpiration.Remove(u.Key);
                                usageStat.Remove(u.Key);
                            });
                        }
                        else
                        {
                            if (expiredFiles.Count == 0)
                            {
                                Directory.GetFiles(dataPath, "*." + ExpirationFileExtension).ForEach(f =>
                                {
                                    var buffer = File.ReadAllText(f);
                                    var expirationDate = Convert.ToDateTime(buffer);
                                    expiredFiles[Path.GetFileNameWithoutExtension(f)] = new KeyValuePair<DateTime, string>(expirationDate, f);
                                });
                            }
                            //find out expired items
                            var fileExpired = expiredFiles.Where(e => e.Value.Key < DateTime.Now);
                            fileExpired.ForEach(u =>
                            {
                                var dataFile = Path.ChangeExtension(u.Value.Value, DataFileExtension);
                                File.Delete(dataFile);
                                File.Delete(u.Value.Value);
                                usageStat.Remove(u.Key);
                                recycledFileCount++;
                            });
                        }

                        //find out least used items
                        var leastUsed = usageStat.OrderByDescending(s => s.Value.Value).Skip(maxCount - threshold);
                        leastUsed.ForEach(u =>
                        {
                            if (isMemory)
                            {
                                memoryData.Remove(u.Key);
                                memoryExpiration.Remove(u.Key);
                            }
                            else
                            {
                                var dataFile = Path.ChangeExtension(u.Value.Key, DataFileExtension);
                                if (File.Exists(dataFile))
                                {
                                    recycledFileCount++;
                                    File.Delete(dataFile);
                                }
                                if (File.Exists(u.Value.Key))
                                    File.Delete(u.Value.Key);
                            }
                            usageStat.Remove(u.Key);
                        });

                        if (!isMemory)
                        {
                            var statFilePath = dataPath + statFile;
                            var count = 0;
                            if (File.Exists(statFilePath))
                            {
                                var buffer = File.ReadAllText(statFilePath);
                                count = Convert.ToInt32(buffer);
                            }
                            count = count - recycledFileCount;
                            if (count < 0)
                                count = 0;
                            File.WriteAllText(statFilePath, count.ToString());
                        }
                    }
                    break;
                default:
                    break;
            }
        }
    }
}